package midas.mw.crt;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import java.util.Enumeration;
import java.util.Iterator;
import java.util.Properties;

import java.util.StringTokenizer;
import java.util.Hashtable;
import java.util.Vector;

import midas.mw.cfg.CfgError;
import midas.mw.cfg.CfgLogging;
import midas.mw.cfg.log.Logger;
import midas.mw.crt.interfaces.CrtCallback;


/**
 * This class is responsible of controlling the network interfaces. 
 * It can start and stop them.
 * It also provides information about neighborhood, nodes presence and all
 * the information related with the network topology
 */
public class NetworkManager extends Thread
{
	
	/**
	 * This hashtable stores information about routing for the wireless interface
	 * in Ad-hoc mode. It has a MIDAS node identifier mapped to its wireless IP 
	 * address. The IP address includes the port configured for communications, so
	 * it has the format 'IP:port'
	 */
	private Hashtable routingTable 			= null;
	
	/**
	 * This hashtable stores information about routing for the wireless interface
	 * in Ad-hoc mode. It has a MIDAS node identifier mapped to its wireless IP 
	 * address. The IP address includes the port configured for communications, so
	 * it has the format 'IP:port'
	 */
	private Hashtable routingTableInv 		= null;
	
	/**
	 * Strores the nodes available in the network for the wireless interface in
	 * Ad-hoc mode. The IP address includes the port configured for communications, so
	 * it has the format 'IP:port'
	 */
	private Vector availableNodes 			= null;
	
	/**
	 * Hashtable where the subscriptions to nodes presence are stored. It has the
	 * destination node identifier mapped to the components waiting for notifications 
	 */
	private Hashtable presenceSubscriptions	= null;
	
	/**
	 * Instance of the CRT running in the node
	 */
	private CrtImpl crt						= null;
	
	/**
     * Variable setting if the NetworkManager subcomponent must go on running or not
     */
    private boolean terminateThread			= true;              
    
	/**
	 * Constructor - initiates local variables
	 * @param crt Instance of the CRT running in the node
	 */
	public NetworkManager(CrtImpl crt)
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager(CrtImpl)");		
		
		this.crt 					= crt;
		presenceSubscriptions 		= new Hashtable();		
		routingTable				= new Hashtable();
		routingTableInv				= new Hashtable();
		availableNodes				= new Vector();
	}
	
	/**
	 * This method registers the request of an component to be notified about the
	 * presence of some node in the network
	 * 
	 * @param crtUser Component requesting the information
	 * @param nodeId MIDAS node identifier of the node for the presence request
	 */
	public void registerPresence(CrtCallback crtUser, String nodeId)
	{	
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.registerPresence(CrtCallback, " + nodeId + ")");			
		
		Vector nodeSubscriptions;
		if (presenceSubscriptions.containsKey(nodeId))
			nodeSubscriptions = (Vector) presenceSubscriptions.get(nodeId);
		else
		{
			nodeSubscriptions = new Vector();
			presenceSubscriptions.put(nodeId, nodeSubscriptions);
		}
		if (!nodeSubscriptions.contains(crtUser))
			nodeSubscriptions.add(crtUser);	
	}
	
	/**
	 * This method cancels the request of an component to be notified about the 
	 * presence of some node in the network
	 * 
	 * @param crtUser Component requesting the information
	 * @param nodeId MIDAS node identifier of the node which presence is requested. 
	 * 			If null the node does not want information about the presence of
	 * 			any node
	 */
	public void unregisterPresence(CrtCallback crtUser, String nodeId)
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.unregisterPresence(CrtCallback, " + nodeId + ")");		
		
		if (nodeId == null)
		{
			String nId 	 		= null;
			Vector listeners 	= null;
			Iterator it 		= presenceSubscriptions.keySet().iterator();
			while (it.hasNext())
			{
				nId = (String) it.next();
				listeners = (Vector) presenceSubscriptions.get(nId);
				if (listeners.contains(crtUser))
					listeners.remove(crtUser);
				if (listeners.size() == 0)
				{
					it.remove();
					//presenceSubscriptions.remove(nId);
					//it = presenceSubscriptions.keySet().iterator();
				}
			}			
		}
		else
		{
			if (presenceSubscriptions.containsKey(nodeId))
			{
				Vector nodeSubscriptions = (Vector) presenceSubscriptions.get(nodeId);
				if (!nodeSubscriptions.remove(crtUser))
					CfgError.errorReporting(CfgError.ERR_INFO, Logger.COMPONENT_CRT, CfgLogging.SEV_INFORMAL, 
							"NetworkManager.unregisterPresence(CrtCallback, " + nodeId + "). This subscription has already been removed", null);					
				
				else
				{
					if (nodeSubscriptions.size() == 0)
						presenceSubscriptions.remove(nodeId);
				}	
			}
			else
				CfgError.errorReporting(CfgError.ERR_INFO, Logger.COMPONENT_CRT, CfgLogging.SEV_INFORMAL, 
						"NetworkManager.unregisterPresence(CrtCallback, " + nodeId + "). This node doesn't have any presence subscription", null);							
		}
	}
	
	/**
	 * This method is called when node presence has changed.
	 * 
	 * @param nodeId MIDAS node identifier of the node for the present request
	 * @param status Status of node. true if the node is in the network, false otherwise
	 * @throws CrtException If an error occurred notifying the presence to the application/component
	 */
	public void notifyPresence(String nodeId, boolean status) throws CrtException
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.notifyPresence(" + nodeId + ", " + status + ")");		
		
		//Inform the message dispatcher about the node presence status
		if (status)
			//Send the stored messages
			//Then the message dispatcher will send the pending messages to the node
			crt.getMessageDispatcher().startSendingMessages(nodeId);
			
						
		//Inform to the applications
		if (presenceSubscriptions.containsKey(nodeId))
		{
			Vector nodeSubscriptions = (Vector) presenceSubscriptions.get(nodeId);
			Iterator iter = nodeSubscriptions.iterator();
			while (iter.hasNext())
			{
				CrtCallback listener = (CrtCallback) iter.next();
				listener.processNodePresence(nodeId, status);				
			}
		}
	}
	
	/**
	 * Returns the associated MIDAS node identifier
	 * 
	 * @return The associated MIDAS node identifier
	 */
	public String getOwnNodeId()
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.getOwnNodeId()");		
		
		//We get the NodeID from the constants		
		return crt.getConstants().getNodeId();
	}
	
	/**
	 * Provides the network information about nodes forming the neighbourhood of the node
	 * In ad hoc wireless networks it will use the information provided by the OLSR protocol.
	 * In infrastructure-based networks it will use the information provided by the Central
	 * Node
	 * 
	 * @param minNumHops Minimum number of hops the requested nodes should be away
	 * @param maxNumHops Maximum number of hops the requested nodes should be away
	 * @return A Properties list. It will be different if the node has a wireless 
	 * 			network interface or not.
	 *			If it has, the list will show the node identifier mapping to the number
	 * 			of hops from the requesting node. Id the MIDAS node identifier can not
	 * 			be retrieved it will show the IP address plus the port for MIDAS
	 * 			communications, 'IP:port'.
	 * 			If not, the list will map the MIDAS node identifier to the first non 
	 * 			Ad-Hoc address. The IP address includes the port configured for 
	 * 			communications, so it will have the format 'IP:port'
	 * @throws CrtException If an error occurred getting the node neighbourhood
	 */
	public Properties getNeighbourhoodInfo(int minNumHops, int maxNumHops) throws CrtException
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.getNeighbourhoodInfo(" + minNumHops + ", " + maxNumHops + ")");		
		
		//We get the nodes forming the neighborhood from routing table		
		Properties neighbourhood 	= new Properties();
		Runtime runtime 			= Runtime.getRuntime();
		Process process				= null;
		InputStream is 				= null;
	    InputStreamReader isr 		= null;
	    BufferedReader br			= null;
		
		//We must distinguish between Ad-hoc mode and Infrastructure mode (or wired mode) 
							
    	if (isInAdHocMode())
    	{
    		String[] cmd = {
    				"/bin/sh",
    				"-c",
    				"/sbin/route | grep " + crt.getConstants().getWiFiInterface() + " | awk '{print $1, $5}'"
    				};		
    		try {			
    			process	= runtime.exec(cmd);
    			is 		= process.getInputStream();
    		    isr 	= new InputStreamReader(is);
    		    br 		= new BufferedReader(isr);
    		    String line;
    		    String ipAddress;
    		    Integer numberOfHops;
    		    while ((line = br.readLine()) != null) 
    		    {		    	
    		    	StringTokenizer tokens 	= new StringTokenizer(line);
    		    	while (tokens.hasMoreTokens()) 
    		    	{
    			    	ipAddress				= tokens.nextToken();
    			    	if (!ipAddress.endsWith(".0"))
    			    	{
    				    	numberOfHops 			= new Integer((String)tokens.nextToken());
    				    	if (minNumHops <= numberOfHops.intValue() || numberOfHops.intValue() <= maxNumHops)
    				    	{
    				    		/*String nodeId = crt.getNodeIdentifierSolver().getMIDASNodeId(ipAddress);
    				    		if (nodeId != null)
    				    			neighbourhood.put(nodeId, numberOfHops);
    				    		else
    				    			neighbourhood.put(ipAddress, numberOfHops);*/
    				    		String nodeId = (String)routingTableInv.get(ipAddress + ":9701");
    				    		if (nodeId != null)
    				    			neighbourhood.put(nodeId, numberOfHops);
    				    		//else
    				    		//	neighbourhood.put(ipAddress + ":9901", numberOfHops);
    				    	}
    			    	}
    			    	else
    			    		break;
    		    	}  	
    		    }		          
    		} catch (IOException ioe) {
    			CfgError.errorReporting(CfgError.ERR_OPER, Logger.COMPONENT_CRT, CfgLogging.SEV_ERROR, "NetworkManager.getNeighbourhoodInfo(" + minNumHops + ", " + maxNumHops + ")" +
    					". An I/O has ocurred getting the neighborhood info: '" + ioe.getMessage() + "'", null);    			    			
    			throw new CrtException();
    		} finally {
    			close(br);
    		    close(isr);
    		    close(is);
    		    process.destroy();		      
    		}
    	}
    	else
    	{
		//if (maxNumHops != minNumHops || maxNumHops < 0)
    		if (maxNumHops < 0)
    			CfgError.errorReporting(CfgError.ERR_INFO, Logger.COMPONENT_CRT, CfgLogging.SEV_INFORMAL, "NetworkManager.getNeighbourhoodInfo(" + minNumHops + ", " + maxNumHops + ")" +
    					". Request not possible", null);			
    			/*CfgError.errorReporting(CfgError.ERR_OPER, "CRT.NetworkManager", CfgError.SEV_INFORMAL, 
    					"getNeighbourhoodInfo(" + minNumHops + ", " + maxNumHops + "). Request not possible"
    					, null);*/
    		else
    		{
    			if (maxNumHops == 0)
		    	{
    				Enumeration e = crt.getNodeIdentifierSolver().getNodeIdToIpConnectedPeers().keys(); 
		    		while (e.hasMoreElements())
		    			neighbourhood.put(e.nextElement(), new Integer(0));
		    	}
    			else
    			{
    				if (crt.getConstants().getNodeType() == crt.getConstants().USER_NODE ||
    						crt.getConstants().getNodeType() == crt.getConstants().NOKIA_USER_NODE)
    		    		//JxtaPlatform().getCentralNodeId() != null)	
		    		{
		    			if (maxNumHops==1)
		    			{
		    				String centralNodeId = crt.getJxtaPlatform().getCentralNodeId();
		    				if (centralNodeId != null)
		    					neighbourhood.put(crt.getJxtaPlatform().getCentralNodeId(), new Integer(1));
		    			}
		    				//neighbourhood = getNonAdHocNodes(neighbourhood);
		    			if (maxNumHops>1)
		    			{
		    				for (Enumeration e=getGatewayNodes().elements(); e.hasMoreElements();)
		    					neighbourhood.put((String)e.nextElement(), new Integer(2));
		    				
		    			}
		    		}
		    		else
		    		{
		    			for (Enumeration e=getGatewayNodes().elements(); e.hasMoreElements();)
	    					neighbourhood.put((String)e.nextElement(), new Integer(1));
		    			Enumeration e2 = crt.getNodeIdentifierSolver().getOwnNormalNodes().elements(); 		    			
			    		while (e2.hasMoreElements())			    		
			    			neighbourhood.put(e2.nextElement(), new Integer(1));			    		
			    		e2 = crt.getNodeIdentifierSolver().getOwnJXMENodes().elements(); 
			    		while (e2.hasMoreElements())
			    			neighbourhood.put(e2.nextElement(), new Integer(1));			    		
		    		}
    			}
    		}	
    	}	    	
		return neighbourhood;
	}
	
	/**
	 * Add an new entry in the internal routing table related to the wireless 
	 * network interface in Ad-Hoc mode. The IP address includes the port 
	 * configured for communications, so it has the format 'IP:port'
	 * 
	 * @param nodeId The MIDAS node identifier of the new node
	 * @param ipAddress The wireless IP address of the new node
	 */
	public void addEntryRoutingTable(String nodeId, String ipAddress)
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.addEntryRoutingTable(" + nodeId  + ", " + ipAddress + ")");		
		
		//String withoutPort = ipAddress.substring(0, ipAddress.indexOf(":"));
		if (routingTableInv.containsKey(ipAddress))
		{
			String nId = (String) routingTableInv.remove(ipAddress);
			//Delete the previous value
			if (nId != null)
				routingTable.remove(nId);
			
		}
		routingTable.put(nodeId, ipAddress);
		routingTableInv.put(ipAddress, nodeId);
		if (!availableNodes.contains(ipAddress))
			availableNodes.add(ipAddress);
	}
	
	/**
	 * Updates the internal routing table related to the wireless network interface
	 * in Ad-hoc mode
	 * 
	 * @throws CrtException If an error occurred updating the wireless routing table
	 */
	private void updateRoutingTable() throws CrtException
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.updateRoutingTable()");		
		
		if (!System.getProperty("os.name").toLowerCase().equals("linux"))
			return;
		
		Vector actualRoutingTable	= new Vector();
		Runtime runtime 			= Runtime.getRuntime();
		Process process				= null;
		InputStream is 				= null;
	    InputStreamReader isr 		= null;
	    BufferedReader br			= null;
	    
		String[] cmd = {
				"/bin/sh",
				"-c",
				"/sbin/route | grep " + crt.getConstants().getWiFiInterface() + " | awk '{print $1}'"
				};		
		try {			
			process					= runtime.exec(cmd);
			is 			= process.getInputStream();
		    isr 		= new InputStreamReader(is);
		    br 			= new BufferedReader(isr);
		    String line;		    
		    while ((line = br.readLine()) != null) 		  
		    	actualRoutingTable.add(line);		
		    
		    //Set keys = routingTable.keySet();
		    //keys.retainAll(actualRoutingTable);
		    for (Enumeration e=routingTableInv.keys();e.hasMoreElements();)
		    {
		    	String ipAddress = (String)e.nextElement();
		    	//Some nodes are not available now
		    	if (!actualRoutingTable.contains(ipAddress.substring(0, ipAddress.indexOf(":")))
		    			&& availableNodes.contains(ipAddress))		    	
		    		availableNodes.remove(ipAddress);		    		
		    	
		    	if (actualRoutingTable.contains(ipAddress.substring(0, ipAddress.indexOf(":")))
		    			&& !availableNodes.contains(ipAddress))
		    	{
		    		//Some nodes are available again
		    		//if (!availableNodes.contains(ipAddress))
		    		availableNodes.add(ipAddress);		    				    	
		    	}
		    }
		    
		    /*
		    Iterator it = availableNodes.iterator();		    
		    System.out.println("________________AVAILABLE NODES________________");
		    while (it.hasNext())		
		    	System.out.println((String)it.next());
		    */
		    
		}catch (IOException ioe) {
			CfgError.errorReporting(CfgError.ERR_OPER, Logger.COMPONENT_CRT, CfgLogging.SEV_ERROR, "NetworkManager.updateRoutingTable()" +
					". An I/O has ocurred updating the routing table: '" + ioe.getMessage() + "'", null);					
			throw new CrtException();
		}finally {
			close(br);
		    close(isr);
		    close(is);
		    process.destroy();		      
		}		
	}
	
	/**	 
	 * Provides the hashtable where is stored the information about the presence
	 * requests of components 
	 * 
	 * @return Hashtable with the nodes for which a presence request has been asked
	 * 			mapped to the components requesting that information
	 */
	public Hashtable getPresenceSubscriptions()
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.getPresenceSubscriptions()");		
		
		return presenceSubscriptions;
	}
	
	/**
	 * Overriding the run method of class Thread
	 */
	public void run()
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.run()");		
		
		terminateThread = false;
		
		while (!terminateThread)
		{	
			try {
				updateRoutingTable();
			} catch (CrtException crte){
			}
			
			try {	
				Thread.sleep(20 * 1000);
			} catch(InterruptedException ie) {				
				ie.printStackTrace();
			}
		}
		
	}
		
	/**
	 * Returns the nodes acting as CNs or Gateway Nodes
	 * @return A vector storing the nodes acting as CNs or Gateway Nodes
	 */
	private Vector getGatewayNodes()
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.getGatewayNodes()");		
		
		return crt.getJxtaPlatform().getSuperNodes();
		//return null;
	}
	
	/**
	 * Checks if a node has a wireless interface configured to work in Ad-hoc
	 * mode
	 * 
	 * @return A value showing if the node has a wireless interface configured 
	 * to work in Ad-hoc mode. True if it has, 0 otherwise
	 * @throws CrtException If an error occurred checking the mode of the wireless 
	 * network interface
	 */
	public boolean isInAdHocMode() throws CrtException
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.isInAdHocMode()");	
		
		if (!System.getProperty("os.name").toLowerCase().equals("linux"))
			return false;
		
		boolean adHocMode			= false;
		Runtime runtime 			= Runtime.getRuntime();
		Process process				= null;
		InputStream is 				= null;
	    InputStreamReader isr 		= null;
	    BufferedReader br			= null;
				
		//We must distinguish between Ad-hoc mode and Infrastructure mode (or wired mode) 
		
		String[] cmd = {
				"/bin/sh",
				"-c",
				"iwgetid -m | awk '{print substr($2,6)}'"
				};	
		try {
			process					= runtime.exec(cmd);
			is 			= process.getInputStream();
		    isr 		= new InputStreamReader(is);
		    br 			= new BufferedReader(isr);
		    String line;
			if ((line = br.readLine()) != null) 
			{
		    	if (line.equalsIgnoreCase("Ad-Hoc"))
		    		adHocMode = true;
			}
		} catch (IOException ioe) {	
			CfgError.errorReporting(CfgError.ERR_OPER, Logger.COMPONENT_CRT, CfgLogging.SEV_ERROR, "NetworkManager.isInAdHocMode()" +
					". An I/O has ocurred getting the wireless mode info: '" + ioe.getMessage() + "'", null);								
			throw new CrtException();
		} finally {
			close(br);
		    close(isr);
		    close(is);
		    process.destroy();		      
		}
		return adHocMode;
	}
	
	/**
	 * Clears all hashtables (routingTable, configuredInterfaces, runningInterfaces
	 * presenceSubscriptions), so that they contains no keys and stops the thread
	 */
	public void stopNetworkManager()
	{
		CfgLogging.logEvent(CfgLogging.LOG_MCALL, Logger.COMPONENT_CRT, CfgLogging.SEV_DEBUG, "NetworkManager.stopNetworkManager()");		
		
		terminateThread = true;
		routingTable.clear();		
		presenceSubscriptions.clear();
	}
	
	private static void close(Closeable c) 
	{
	    if (c != null) 
	    {
	      try {
	        c.close();
	      } catch (IOException e) {
	        // ignored
	      }
	    }
	}
}
